<template>
  <ul class="w-list" :class="classes">
    <li
      class="w-list__item"
      v-for="(li, i) in listItems"
      :key="i"
      :class="{ 'w-list__item--parent': (li.children || []).length }"
    >
      <w-icon class="w-list__item-bullet" v-if="icon">{{ icon }}</w-icon>
      <component
        class="w-list__item-label"
        v-if="$slots[`item.${i + 1}`] || $slots.item || $slots.default"
        :is="
          checklist
            ? 'w-checkbox'
            : nav && !li.disabled && li.route
            ? hasRouter
              ? 'router-link'
              : 'a'
            : 'div'
        "
        v-bind="liLabelProps(li, i, li._selected)"
      >
        <slot
          v-if="$slots[`item.${i + 1}`]"
          :name="`item.${i + 1}`"
          :item="cleanLi(li)"
          :index="i + 1"
          :selected="li._selected"
        ></slot>
        <slot
          v-else-if="$slots.item"
          name="item"
          :item="cleanLi(li)"
          :index="i + 1"
          :selected="li._selected"
        ></slot>
        <slot
          v-else
          :item="cleanLi(li)"
          :index="i + 1"
          :selected="li._selected"
          >{{ li._label }}</slot
        >
      </component>
      <component
        class="w-list__item-label"
        v-else
        :is="
          checklist
            ? 'w-checkbox'
            : nav && !li.disabled && li.route
            ? hasRouter
              ? 'router-link'
              : 'a'
            : 'div'
        "
        v-bind="liLabelProps(li, i, li._selected)"
      ></component>
      <w-list
        v-if="(li.children || []).length"
        v-bind="$props"
        :items="li.children"
        :depth="depth + 1"
        @update:model-value="$emit('update:modelValue', $event)"
        @input="$emit('input', $event)"
        @item-click="$emit('item-click', $event)"
        @item-select="$emit('item-select', $event)"
        ><template v-if="$slots.item" #item="{ item, index, selected }">
          <slot
            name="item"
            :item="cleanLi(item)"
            :index="index"
            :selected="selected"
          ></slot> </template
        ><template v-else #default="{ item, index, selected }">
          <slot :item="cleanLi(item)" :index="index" :selected="selected">{{
            item[itemLabelKey]
          }}</slot>
        </template></w-list
      >
    </li>
  </ul>
</template>

<script>
  import { ref, computed, getCurrentInstance, watch } from "vue";
  export default {
    name: "w-list",

    props: {
      items: { type: [Array, Number], required: true }, // All the possible options.
      modelValue: {}, // v-model on selected item if any.
      checklist: { type: Boolean },
      roundCheckboxes: { type: Boolean }, // Checklist option.
      // If selectable (if value !== false), this allows multiple selections.
      multiple: { type: Boolean },
      // When true, will add an id on the list and on all the list items.
      // Useful for a11y aria fields (e.g. use with w-select).
      addIds: { type: [Boolean, String] },
      hover: { type: Boolean },
      color: { type: String }, // Applies to all the items.
      selectionColor: { type: String }, // Applies to the selected items only.
      bgColor: { type: String }, // Applies to all the items.
      // Navigation type adds a router-link on items with `route`.
      nav: { type: Boolean },
      icon: { type: String, default: "" },
      itemLabelKey: { type: String, default: "label" }, // Name of the label field.
      itemValueKey: { type: String, default: "value" }, // Name of the value field.
      itemColorKey: { type: String, default: "color" }, // Support a different color per item.
      itemClass: { type: String },
      depth: { type: Number, default: 0 }, // For recursive call.
      returnObject: { type: Boolean },
      // By default you can unselect a list item by re-selecting it.
      // Allow preventing that on single selection lists only.
      noUnselect: { type: Boolean },
      arrowsNavigation: { type: Boolean },
    },

    emits: [
      "input",
      "update:modelValue",
      "item-click",
      "item-select",
      "keydown:escape",
      "keydown:enter",
    ],

    setup(props, ctx) {
      const instance = getCurrentInstance();
      const listItems = ref([]);

      refreshListItems();
      applySelectionOnItems(props.modelValue);

      const hasRouter = computed(() => {
        return "$router" in instance.ctx;
      });

      const listId = computed(() => {
        return props.addIds
          ? typeof props.addIds === "string"
            ? props.addIds
            : `w-list--${instance.ctx._.uid}`
          : null;
      });

      const selectedItems = computed(() => {
        return listItems.value.filter((item) => item._selected);
      });

      // Faster cached enabled items lookup.
      const enabledItemsIndexes = computed(() => {
        return listItems.value
          .filter((item) => !item.disabled)
          .map((item) => item.index);
      });

      const isMultipleSelect = computed(() => {
        return props.multiple || props.checklist; // Checklist is always multiple select.
      });

      const isSelectable = computed(() => {
        return props.modelValue !== undefined || props.checklist || props.nav;
      });

      const SelectionColor = computed(() => {
        // Only if no color & no selectionColor is set, set the selectionColor to primary.
        const selectionColor =
          props.selectionColor === undefined
            ? !props.color && "primary"
            : props.selectionColor;
        return isSelectable.value && selectionColor;
      });

      const classes = computed(() => {
        return {
          [props.color]: props.color || null,
          [`${props.bgColor}--bg`]: props.bgColor || null,
          "w-list--checklist": props.checklist,
          "w-list--navigation": props.nav,
          "w-list--icon": props.icon,
          [`w-list--child w-list--depth-${props.depth}`]: props.depth,
        };
      });
      function getItemValue(item) {
        if (item && typeof item === "object") {
          if (item[props.itemValueKey] !== undefined) {
            return item[props.itemValueKey];
          } else {
            return item[props.itemLabelKey] !== undefined
              ? item[props.itemLabelKey]
              : item.index;
          }
        } else {
          return item;
        }
      }

      // Action of selecting 1 item.
      function selectItem(item, forcedValue) {
        // Prevent unselecting a selected item if noUnselect is true in single selection list.
        if (item._selected && !props.multiple && props.noUnselect) return;

        // Select or unselect the item.
        item._selected =
          forcedValue !== undefined ? forcedValue : !item._selected;

        // If not multiple selection and just selected an item, unselect any other.
        if (item._selected && !isMultipleSelect.value) {
          listItems.value.forEach(
            (i) => i._index !== item._index && (i._selected = false)
          );
        }
        emitSelection();
      }

      function liLabelClasses(item) {
        return {
          "w-list__item-label--disabled":
            item.disabled || (props.nav && !item.route && !item.children),
          "w-list__item-label--active":
            (isSelectable.value && item._selected) || null,
          "w-list__item-label--focused": item._focused,
          "w-list__item-label--hoverable": props.hover,
          "w-list__item-label--selectable": isSelectable.value,
          [item.color]: !!item.color,
          [SelectionColor.value]:
            item._selected && !item.color && SelectionColor.value,
          [props.itemClass]: !!props.itemClass,
        };
      }

      function liLabelProps(li, index, selected) {
        const hasSlot = ctx.slots[`item.${index + 1}`] || ctx.slots.item;

        // Event handlers.
        // ------------------------------------------------------
        const click = () => {
          if (!li.disabled) {
            const varcleanLi = cleanLi(li);
            ctx.emit("item-click", varcleanLi);
            ctx.emit("item-select", varcleanLi);
          }
        };
        // If selectable list, on mousedown select the item.
        const mousedown =
          isSelectable.value &&
          ((e) => {
            e.stopPropagation();
            !li.disabled && selectItem(li);
          });
        // If selectable list, on enter key press select item.
        const keydown =
          isSelectable.value &&
          ((e) => {
            if (!li.disabled && e.keyCode === 13) {
              selectItem(li);
              // eslint-disable-next-line vue/custom-event-name-casing
              ctx.emit("keydown:enter");
              ctx.emit("item-select", cleanLi(li));
            }
            // eslint-disable-next-line vue/custom-event-name-casing
            else if (e.keyCode === 27) ctx.emit("keydown:escape");
            else if (props.arrowsNavigation) {
              e.preventDefault();
              if (e.keyCode === 38) {
                focusPrevNextItem(li._index, false);
              }
              if (e.keyCode === 40) {
                focusPrevNextItem(li._index, true);
              }
            }
          });
        // ------------------------------------------------------

        const propsObj = {
          class: liLabelClasses(li),
          tabindex: li.disabled || props.checklist ? null : "0",
          "aria-selected": selected ? "true" : "false",
          id: listId.value ? `${listId.value}_item-${index + 1}` : null,
          role: "option",
        };

        // Checklist.
        // ------------------------------------------------------
        if (props.checklist) {
          propsObj.modelValue = li._selected;
          propsObj.color = li[props.itemColorKey] || props.color;
          propsObj.round = props.roundCheckboxes;
          propsObj.disabled = li.disabled;

          if (!hasSlot) propsObj.label = li._label || null;

          propsObj.onFocus = () => (li._focused = true);
          propsObj.onBlur = () => (li._focused = false);
          propsObj.onInput = (value) => selectItem(li, value);
          // When clicking on the checkbox component wrapper, trigger a focus & click on the checkbox.
          propsObj.onClick = (e) => {
            const checkbox = e.target.querySelector('input[type="checkbox"]');
            if (checkbox) {
              checkbox.focus();
              checkbox.click();
            }

            click(); // Emit the `item-click` & `item-select` events.
          };
        }
        // ------------------------------------------------------

        // Navigation list.
        // Note: on enter key press, a click event is fired => this is default HTML behavior.
        // ------------------------------------------------------
        else if (props.nav) {
          if (!li.disabled && li.route) {
            propsObj.onKeydown = keydown;
            propsObj.onMousedown = mousedown;

            if (instance.ctx.$router) {
              propsObj.to = li.route;
              // In HTML5 history mode, Vue 3 router-link will intercept the click event so that the browser
              // doesn't try to reload the page.
              // (in Vue 2, the click event was on `nativeOn`, since in Vue 3 the component options/props
              // definitions are flattened the issue appears)
              // So in Vue 3, we can either use the custom prop and pass a default slot and create the
              // `a` link ourselves, or call preventDefault & `$router.push` directly which is done
              // internally by vue-router.
              propsObj.onClick = (e) => {
                e.preventDefault();
                instance.ctx.$router.push(li.route);
                click(); // Emit the `item-click` & `item-select` events.
              };
            } else {
              propsObj.href = li.route;
              propsObj.onClick = click;
            }
          }

          if (!hasSlot) propsObj.innerHTML = li._label;
        }
        // ------------------------------------------------------

        // Selectable simple div.
        // ------------------------------------------------------
        else if (isSelectable.value) {
          // Links are naturally tabable, add tabindex on other elements.
          if (!li.disabled) propsObj.tabindex = 0;
          propsObj.onClick = click;
          propsObj.onKeydown = keydown;
          propsObj.onMousedown = mousedown;
          if (!hasSlot) {
            propsObj.innerHTML = li._label;
          }
        }
        // ------------------------------------------------------
        else if (!hasSlot) {
          propsObj.innerHTML = li._label;
        }

        return propsObj;
      }

      // // Convert the received items selection to array if it is a unique value.
      // // Also accept objects if returnObject is true and convert to the object's value.
      // // In any case, always end up with an array of values.
      // // The values given can be (in this order) a value, a label or the index of the item.
      function checkSelection(items) {
        items = Array.isArray(items) ? items : items ? [items] : [];
        // TODO: getItemValue is Function
        if (props.returnObject) {
          items = items.map(getItemValue);
        }

        return items;
      }

      // Emit the cleaned-up selection to the v-model.
      function emitSelection() {
        const items = selectedItems.value.map((item) => {
          if (!props.returnObject) {
            return item._value;
          }
          // eslint-disable-next-line no-unused-vars
          const { _value, _selected, ...Item } = item;
          return Item;
        });

        // `selectedItems` is always an array of items, but on set, it emits a single value if not `multiple`.
        const selection = isMultipleSelect.value
          ? items
          : items[0] !== undefined
          ? items[0]
          : null;
        ctx.emit("update:modelValue", selection);
        ctx.emit("input", selection);
      }

      function focusPrevNextItem(index, next = true) {
        // The index of the previous or next item in the array of enabled items.
        index =
          enabledItemsIndexes.value[
            enabledItemsIndexes.value.indexOf(index) + (next ? 1 : -1)
          ];
        const firstOrLastIndex = next
          ? 0
          : enabledItemsIndexes.value.length - 1;
        if (index === undefined)
          index = enabledItemsIndexes.value[firstOrLastIndex];

        instance.ctx.$el
          .querySelector(`#${listId.value}_item-${index + 1}`)
          .focus();
      }

      function cleanLi(li) {
        // eslint-disable-next-line no-unused-vars
        const { _index, _value, _label, _selected, _focused, ...cleanLi } = li;
        return cleanLi;
      }

      function refreshListItems() {
        const items =
          typeof props.items === "number"
            ? Array(props.items).fill({})
            : props.items || [];
        listItems.value = items.map((item, i) => ({
          ...item,
          _index: i,
          // If no value is set on the item, add one from its label, or from its index.
          // The result is store in a _value attribute.
          _value:
            item[props.itemValueKey] === undefined
              ? item[props.itemLabelKey] || i
              : item[props.itemValueKey],
          _selected: item._selected || false,
          _label: item[props.itemLabelKey] || "",
          _focused: false,
        }));
      }

      function applySelectionOnItems(selection) {
        checkSelection(selection) // Create an array with the selected values.
          .forEach(
            (val) =>
              (listItems.value.find(
                (item) => item._value === val
              )._selected = true)
          );
      }
      watch(
        () => props.items,
        () => {
          refreshListItems();
          applySelectionOnItems(props.modelValue);
        }
      );
      watch(
        () => props.modelValue,
        (items) => {
          applySelectionOnItems(items);
        }
      );
      watch(
        () => props.multiple,
        (boolean) => {
          if (!boolean) {
            let firstSelected = null;
            listItems.value.forEach((item) => {
              if (item._selected && !firstSelected) firstSelected = item;
              else if (item._selected) item._selected = false;
            });
            emitSelection();
          }
        }
      );

      return {
        listItems,
        // computed
        hasRouter,
        listId,
        selectedItems,
        enabledItemsIndexes,
        isMultipleSelect,
        isSelectable,
        SelectionColor,
        classes,
        // methods
        getItemValue,
        selectItem,
        liLabelClasses,
        liLabelProps,
        checkSelection,
        emitSelection,
        focusPrevNextItem,
        cleanLi,
        refreshListItems,
        applySelectionOnItems,
      };
    },
  };
</script>
